/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Haystack Software Inc. All rights reserved.
 *  Licensed under the PolyForm Strict License 1.0.0. See License.txt in the project root for
 *  license information.
 *--------------------------------------------------------------------------------------------*/

/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See code-license.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import { coalesce } from "vs/base/common/arrays"
import { Promises, ResourceQueue } from "vs/base/common/async"
import {
  bufferedStreamToBuffer,
  bufferToReadable,
  newWriteableBufferStream,
  readableToBuffer,
  streamToBuffer,
  VSBuffer,
  VSBufferReadable,
  VSBufferReadableBufferedStream,
  VSBufferReadableStream,
} from "vs/base/common/buffer"
import {
  CancellationToken,
  CancellationTokenSource,
} from "vs/base/common/cancellation"
import { Emitter } from "vs/base/common/event"
import { hash } from "vs/base/common/hash"
import { Iterable } from "vs/base/common/iterator"
import {
  Disposable,
  DisposableStore,
  dispose,
  IDisposable,
  toDisposable,
} from "vs/base/common/lifecycle"
import { TernarySearchTree } from "vs/base/common/ternarySearchTree"
import { Schemas } from "vs/base/common/network"
import { mark } from "vs/base/common/performance"
import {
  extUri,
  extUriIgnorePathCase,
  IExtUri,
  isAbsolutePath,
} from "vs/base/common/resources"
import {
  consumeStream,
  isReadableBufferedStream,
  isReadableStream,
  listenStream,
  newWriteableStream,
  peekReadable,
  peekStream,
  transform,
} from "vs/base/common/stream"
import { URI } from "vs/base/common/uri"
import { localize } from "vs/nls"
import {
  ensureFileSystemProviderError,
  etag,
  ETAG_DISABLED,
  FileChangesEvent,
  IFileDeleteOptions,
  FileOperation,
  FileOperationError,
  FileOperationEvent,
  FileOperationResult,
  FilePermission,
  FileSystemProviderCapabilities,
  FileSystemProviderErrorCode,
  FileType,
  hasFileAtomicReadCapability,
  hasFileFolderCopyCapability,
  hasFileReadStreamCapability,
  hasOpenReadWriteCloseCapability,
  hasReadWriteCapability,
  ICreateFileOptions,
  IFileContent,
  IFileService,
  IFileStat,
  IFileStatWithMetadata,
  IFileStreamContent,
  IFileSystemProvider,
  IFileSystemProviderActivationEvent,
  IFileSystemProviderCapabilitiesChangeEvent,
  IFileSystemProviderRegistrationEvent,
  IFileSystemProviderWithFileAtomicReadCapability,
  IFileSystemProviderWithFileReadStreamCapability,
  IFileSystemProviderWithFileReadWriteCapability,
  IFileSystemProviderWithOpenReadWriteCloseCapability,
  IReadFileOptions,
  IReadFileStreamOptions,
  IResolveFileOptions,
  IFileStatResult,
  IFileStatResultWithMetadata,
  IResolveMetadataFileOptions,
  IStat,
  IFileStatWithPartialMetadata,
  IWatchOptions,
  IWriteFileOptions,
  NotModifiedSinceFileOperationError,
  toFileOperationResult,
  toFileSystemProviderErrorCode,
  hasFileCloneCapability,
  TooLargeFileOperationError,
  hasFileAtomicDeleteCapability,
  hasFileAtomicWriteCapability,
  IWatchOptionsWithCorrelation,
  IFileSystemWatcher,
  IWatchOptionsWithoutCorrelation,
} from "vs/platform/files/common/files"
import { readFileIntoStream } from "vs/platform/files/common/io"
import { ILogService } from "vs/platform/log/common/log"
import { ErrorNoTelemetry } from "vs/base/common/errors"

export class FileService extends Disposable implements IFileService {
  declare readonly _serviceBrand: undefined

  // Choose a buffer size that is a balance between memory needs and
  // manageable IPC overhead. The larger the buffer size, the less
  // roundtrips we have to do for reading/writing data.
  private readonly BUFFER_SIZE = 256 * 1024

  constructor(@ILogService private readonly logService: ILogService) {
    super()
  }

  //#region File System Provider

  private readonly _onDidChangeFileSystemProviderRegistrations = this._register(
    new Emitter<IFileSystemProviderRegistrationEvent>()
  )
  readonly onDidChangeFileSystemProviderRegistrations =
    this._onDidChangeFileSystemProviderRegistrations.event

  private readonly _onWillActivateFileSystemProvider = this._register(
    new Emitter<IFileSystemProviderActivationEvent>()
  )
  readonly onWillActivateFileSystemProvider =
    this._onWillActivateFileSystemProvider.event

  private readonly _onDidChangeFileSystemProviderCapabilities = this._register(
    new Emitter<IFileSystemProviderCapabilitiesChangeEvent>()
  )
  readonly onDidChangeFileSystemProviderCapabilities =
    this._onDidChangeFileSystemProviderCapabilities.event

  private readonly provider = new Map<string, IFileSystemProvider>()

  registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable {
    if (this.provider.has(scheme)) {
      throw new Error(
        `A filesystem provider for the scheme '${scheme}' is already registered.`
      )
    }

    mark(`code/registerFilesystem/${scheme}`)

    const providerDisposables = new DisposableStore()

    // Add provider with event
    this.provider.set(scheme, provider)
    this._onDidChangeFileSystemProviderRegistrations.fire({
      added: true,
      scheme,
      provider,
    })

    // Forward events from provider
    providerDisposables.add(
      provider.onDidChangeFile((changes) => {
        const event = new FileChangesEvent(
          changes,
          !this.isPathCaseSensitive(provider)
        )

        // Always emit any event internally
        this.internalOnDidFilesChange.fire(event)

        // Only emit uncorrelated events in the global `onDidFilesChange` event
        if (!event.hasCorrelation()) {
          this._onDidUncorrelatedFilesChange.fire(event)
        }
      })
    )
    if (typeof provider.onDidWatchError === "function") {
      providerDisposables.add(
        provider.onDidWatchError((error) =>
          this._onDidWatchError.fire(new Error(error))
        )
      )
    }
    providerDisposables.add(
      provider.onDidChangeCapabilities(() =>
        this._onDidChangeFileSystemProviderCapabilities.fire({
          provider,
          scheme,
        })
      )
    )

    return toDisposable(() => {
      this._onDidChangeFileSystemProviderRegistrations.fire({
        added: false,
        scheme,
        provider,
      })
      this.provider.delete(scheme)

      dispose(providerDisposables)
    })
  }

  getProvider(scheme: string): IFileSystemProvider | undefined {
    return this.provider.get(scheme)
  }

  async activateProvider(scheme: string): Promise<void> {
    // Emit an event that we are about to activate a provider with the given scheme.
    // Listeners can participate in the activation by registering a provider for it.
    const joiners: Promise<void>[] = []
    this._onWillActivateFileSystemProvider.fire({
      scheme,
      join(promise) {
        joiners.push(promise)
      },
    })

    if (this.provider.has(scheme)) {
      return // provider is already here so we can return directly
    }

    // If the provider is not yet there, make sure to join on the listeners assuming
    // that it takes a bit longer to register the file system provider.
    await Promises.settled(joiners)
  }

  async canHandleResource(resource: URI): Promise<boolean> {
    // Await activation of potentially extension contributed providers
    await this.activateProvider(resource.scheme)

    return this.hasProvider(resource)
  }

  hasProvider(resource: URI): boolean {
    return this.provider.has(resource.scheme)
  }

  hasCapability(
    resource: URI,
    capability: FileSystemProviderCapabilities
  ): boolean {
    const provider = this.provider.get(resource.scheme)

    return !!(provider && provider.capabilities & capability)
  }

  listCapabilities(): Iterable<{
    scheme: string
    capabilities: FileSystemProviderCapabilities
  }> {
    return Iterable.map(this.provider, ([scheme, provider]) => ({
      scheme,
      capabilities: provider.capabilities,
    }))
  }

  protected async withProvider(resource: URI): Promise<IFileSystemProvider> {
    // Assert path is absolute
    if (!isAbsolutePath(resource)) {
      throw new FileOperationError(
        localize(
          "invalidPath",
          "Unable to resolve filesystem provider with relative file path '{0}'",
          this.resourceForError(resource)
        ),
        FileOperationResult.FILE_INVALID_PATH
      )
    }

    // Activate provider
    await this.activateProvider(resource.scheme)

    // Assert provider
    const provider = this.provider.get(resource.scheme)
    if (!provider) {
      const error = new ErrorNoTelemetry()
      error.message = localize(
        "noProviderFound",
        "ENOPRO: No file system provider found for resource '{0}'",
        resource.toString()
      )

      throw error
    }

    return provider
  }

  private async withReadProvider(
    resource: URI
  ): Promise<
    | IFileSystemProviderWithFileReadWriteCapability
    | IFileSystemProviderWithOpenReadWriteCloseCapability
    | IFileSystemProviderWithFileReadStreamCapability
  > {
    const provider = await this.withProvider(resource)

    if (
      hasOpenReadWriteCloseCapability(provider) ||
      hasReadWriteCapability(provider) ||
      hasFileReadStreamCapability(provider)
    ) {
      return provider
    }

    throw new Error(
      `Filesystem provider for scheme '${resource.scheme}' neither has FileReadWrite, FileReadStream nor FileOpenReadWriteClose capability which is needed for the read operation.`
    )
  }

  private async withWriteProvider(
    resource: URI
  ): Promise<
    | IFileSystemProviderWithFileReadWriteCapability
    | IFileSystemProviderWithOpenReadWriteCloseCapability
  > {
    const provider = await this.withProvider(resource)

    if (
      hasOpenReadWriteCloseCapability(provider) ||
      hasReadWriteCapability(provider)
    ) {
      return provider
    }

    throw new Error(
      `Filesystem provider for scheme '${resource.scheme}' neither has FileReadWrite nor FileOpenReadWriteClose capability which is needed for the write operation.`
    )
  }

  //#endregion

  //#region Operation events

  private readonly _onDidRunOperation = this._register(
    new Emitter<FileOperationEvent>()
  )
  readonly onDidRunOperation = this._onDidRunOperation.event

  //#endregion

  //#region File Metadata Resolving

  async resolve(
    resource: URI,
    options: IResolveMetadataFileOptions
  ): Promise<IFileStatWithMetadata>
  async resolve(
    resource: URI,
    options?: IResolveFileOptions
  ): Promise<IFileStat>
  async resolve(
    resource: URI,
    options?: IResolveFileOptions
  ): Promise<IFileStat> {
    try {
      return await this.doResolveFile(resource, options)
    } catch (error) {
      // Specially handle file not found case as file operation result
      if (
        toFileSystemProviderErrorCode(error) ===
        FileSystemProviderErrorCode.FileNotFound
      ) {
        throw new FileOperationError(
          localize(
            "fileNotFoundError",
            "Unable to resolve nonexistent file '{0}'",
            this.resourceForError(resource)
          ),
          FileOperationResult.FILE_NOT_FOUND
        )
      }

      // Bubble up any other error as is
      throw ensureFileSystemProviderError(error)
    }
  }

  private async doResolveFile(
    resource: URI,
    options: IResolveMetadataFileOptions
  ): Promise<IFileStatWithMetadata>
  private async doResolveFile(
    resource: URI,
    options?: IResolveFileOptions
  ): Promise<IFileStat>
  private async doResolveFile(
    resource: URI,
    options?: IResolveFileOptions
  ): Promise<IFileStat> {
    const provider = await this.withProvider(resource)
    const isPathCaseSensitive = this.isPathCaseSensitive(provider)

    const resolveTo = options?.resolveTo
    const resolveSingleChildDescendants = options?.resolveSingleChildDescendants
    const resolveMetadata = options?.resolveMetadata

    const stat = await provider.stat(resource)

    let trie: TernarySearchTree<URI, boolean> | undefined

    return this.toFileStat(
      provider,
      resource,
      stat,
      undefined,
      !!resolveMetadata,
      (stat, siblings) => {
        // lazy trie to check for recursive resolving
        if (!trie) {
          trie = TernarySearchTree.forUris<true>(() => !isPathCaseSensitive)
          trie.set(resource, true)
          if (resolveTo) {
            trie.fill(true, resolveTo)
          }
        }

        // check for recursive resolving
        if (
          trie.get(stat.resource) ||
          trie.findSuperstr(
            stat.resource.with(
              {
                query: null,
                fragment: null,
              } /* required for https://github.com/microsoft/vscode/issues/128151 */
            )
          )
        ) {
          return true
        }

        // check for resolving single child folders
        if (stat.isDirectory && resolveSingleChildDescendants) {
          return siblings === 1
        }

        return false
      }
    )
  }

  private async toFileStat(
    provider: IFileSystemProvider,
    resource: URI,
    stat: IStat | ({ type: FileType } & Partial<IStat>),
    siblings: number | undefined,
    resolveMetadata: boolean,
    recurse: (stat: IFileStat, siblings?: number) => boolean
  ): Promise<IFileStat>
  private async toFileStat(
    provider: IFileSystemProvider,
    resource: URI,
    stat: IStat,
    siblings: number | undefined,
    resolveMetadata: true,
    recurse: (stat: IFileStat, siblings?: number) => boolean
  ): Promise<IFileStatWithMetadata>
  private async toFileStat(
    provider: IFileSystemProvider,
    resource: URI,
    stat: IStat | ({ type: FileType } & Partial<IStat>),
    siblings: number | undefined,
    resolveMetadata: boolean,
    recurse: (stat: IFileStat, siblings?: number) => boolean
  ): Promise<IFileStat> {
    const { providerExtUri } = this.getExtUri(provider)

    // convert to file stat
    const fileStat: IFileStat = {
      resource,
      name: providerExtUri.basename(resource),
      isFile: (stat.type & FileType.File) !== 0,
      isDirectory: (stat.type & FileType.Directory) !== 0,
      isSymbolicLink: (stat.type & FileType.SymbolicLink) !== 0,
      mtime: stat.mtime,
      ctime: stat.ctime,
      size: stat.size,
      readonly:
        Boolean((stat.permissions ?? 0) & FilePermission.Readonly) ||
        Boolean(
          provider.capabilities & FileSystemProviderCapabilities.Readonly
        ),
      locked: Boolean((stat.permissions ?? 0) & FilePermission.Locked),
      etag: etag({ mtime: stat.mtime, size: stat.size }),
      children: undefined,
    }

    // check to recurse for directories
    if (fileStat.isDirectory && recurse(fileStat, siblings)) {
      try {
        const entries = await provider.readdir(resource)
        const resolvedEntries = await Promises.settled(
          entries.map(async ([name, type]) => {
            try {
              const childResource = providerExtUri.joinPath(resource, name)
              const childStat = resolveMetadata
                ? await provider.stat(childResource)
                : { type }

              return await this.toFileStat(
                provider,
                childResource,
                childStat,
                entries.length,
                resolveMetadata,
                recurse
              )
            } catch (error) {
              this.logService.trace(error)

              return null // can happen e.g. due to permission errors
            }
          })
        )

        // make sure to get rid of null values that signal a failure to resolve a particular entry
        fileStat.children = coalesce(resolvedEntries)
      } catch (error) {
        this.logService.trace(error)

        fileStat.children = [] // gracefully handle errors, we may not have permissions to read
      }

      return fileStat
    }

    return fileStat
  }

  async resolveAll(
    toResolve: { resource: URI; options?: IResolveFileOptions }[]
  ): Promise<IFileStatResult[]>
  async resolveAll(
    toResolve: { resource: URI; options: IResolveMetadataFileOptions }[]
  ): Promise<IFileStatResultWithMetadata[]>
  async resolveAll(
    toResolve: { resource: URI; options?: IResolveFileOptions }[]
  ): Promise<IFileStatResult[]> {
    return Promises.settled(
      toResolve.map(async (entry) => {
        try {
          return {
            stat: await this.doResolveFile(entry.resource, entry.options),
            success: true,
          }
        } catch (error) {
          this.logService.trace(error)

          return { stat: undefined, success: false }
        }
      })
    )
  }

  async stat(resource: URI): Promise<IFileStatWithPartialMetadata> {
    const provider = await this.withProvider(resource)

    const stat = await provider.stat(resource)

    return this.toFileStat(
      provider,
      resource,
      stat,
      undefined,
      true,
      () => false /* Do not resolve any children */
    )
  }

  async exists(resource: URI): Promise<boolean> {
    const provider = await this.withProvider(resource)

    try {
      const stat = await provider.stat(resource)

      return !!stat
    } catch (error) {
      return false
    }
  }

  //#endregion

  //#region File Reading/Writing

  async canCreateFile(
    resource: URI,
    options?: ICreateFileOptions
  ): Promise<Error | true> {
    try {
      await this.doValidateCreateFile(resource, options)
    } catch (error) {
      return error
    }

    return true
  }

  private async doValidateCreateFile(
    resource: URI,
    options?: ICreateFileOptions
  ): Promise<void> {
    // validate overwrite
    if (!options?.overwrite && (await this.exists(resource))) {
      throw new FileOperationError(
        localize(
          "fileExists",
          "Unable to create file '{0}' that already exists when overwrite flag is not set",
          this.resourceForError(resource)
        ),
        FileOperationResult.FILE_MODIFIED_SINCE,
        options
      )
    }
  }

  async createFile(
    resource: URI,
    bufferOrReadableOrStream:
      | VSBuffer
      | VSBufferReadable
      | VSBufferReadableStream = VSBuffer.fromString(""),
    options?: ICreateFileOptions
  ): Promise<IFileStatWithMetadata> {
    // validate
    await this.doValidateCreateFile(resource, options)

    // do write into file (this will create it too)
    const fileStat = await this.writeFile(resource, bufferOrReadableOrStream)

    // events
    this._onDidRunOperation.fire(
      new FileOperationEvent(resource, FileOperation.CREATE, fileStat)
    )

    return fileStat
  }

  async writeFile(
    resource: URI,
    bufferOrReadableOrStream:
      | VSBuffer
      | VSBufferReadable
      | VSBufferReadableStream,
    options?: IWriteFileOptions
  ): Promise<IFileStatWithMetadata> {
    const provider = this.throwIfFileSystemIsReadonly(
      await this.withWriteProvider(resource),
      resource
    )
    const { providerExtUri } = this.getExtUri(provider)

    let writeFileOptions = options
    if (hasFileAtomicWriteCapability(provider) && !writeFileOptions?.atomic) {
      const enforcedAtomicWrite = provider.enforceAtomicWriteFile?.(resource)
      if (enforcedAtomicWrite) {
        writeFileOptions = { ...options, atomic: enforcedAtomicWrite }
      }
    }

    try {
      // validate write
      const stat = await this.validateWriteFile(
        provider,
        resource,
        writeFileOptions
      )

      // mkdir recursively as needed
      if (!stat) {
        await this.mkdirp(provider, providerExtUri.dirname(resource))
      }

      // optimization: if the provider has unbuffered write capability and the data
      // to write is not a buffer, we consume up to 3 chunks and try to write the data
      // unbuffered to reduce the overhead. If the stream or readable has more data
      // to provide we continue to write buffered.
      let bufferOrReadableOrStreamOrBufferedStream:
        | VSBuffer
        | VSBufferReadable
        | VSBufferReadableStream
        | VSBufferReadableBufferedStream
      if (
        hasReadWriteCapability(provider) &&
        !(bufferOrReadableOrStream instanceof VSBuffer)
      ) {
        if (isReadableStream(bufferOrReadableOrStream)) {
          const bufferedStream = await peekStream(bufferOrReadableOrStream, 3)
          if (bufferedStream.ended) {
            bufferOrReadableOrStreamOrBufferedStream = VSBuffer.concat(
              bufferedStream.buffer
            )
          } else {
            bufferOrReadableOrStreamOrBufferedStream = bufferedStream
          }
        } else {
          bufferOrReadableOrStreamOrBufferedStream = peekReadable(
            bufferOrReadableOrStream,
            (data) => VSBuffer.concat(data),
            3
          )
        }
      } else {
        bufferOrReadableOrStreamOrBufferedStream = bufferOrReadableOrStream
      }

      // write file: unbuffered
      if (
        !hasOpenReadWriteCloseCapability(provider) || // buffered writing is unsupported
        (hasReadWriteCapability(provider) &&
          bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer) || // data is a full buffer already
        (hasReadWriteCapability(provider) &&
          hasFileAtomicWriteCapability(provider) &&
          writeFileOptions?.atomic) // atomic write forces unbuffered write if the provider supports it
      ) {
        await this.doWriteUnbuffered(
          provider,
          resource,
          writeFileOptions,
          bufferOrReadableOrStreamOrBufferedStream
        )
      }

      // write file: buffered
      else {
        await this.doWriteBuffered(
          provider,
          resource,
          writeFileOptions,
          bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer
            ? bufferToReadable(bufferOrReadableOrStreamOrBufferedStream)
            : bufferOrReadableOrStreamOrBufferedStream
        )
      }

      // events
      this._onDidRunOperation.fire(
        new FileOperationEvent(resource, FileOperation.WRITE)
      )
    } catch (error) {
      throw new FileOperationError(
        localize(
          "err.write",
          "Unable to write file '{0}' ({1})",
          this.resourceForError(resource),
          ensureFileSystemProviderError(error).toString()
        ),
        toFileOperationResult(error),
        writeFileOptions
      )
    }

    return this.resolve(resource, { resolveMetadata: true })
  }

  private async validateWriteFile(
    provider: IFileSystemProvider,
    resource: URI,
    options?: IWriteFileOptions
  ): Promise<IStat | undefined> {
    // Validate unlock support
    const unlock = !!options?.unlock
    if (
      unlock &&
      !(provider.capabilities & FileSystemProviderCapabilities.FileWriteUnlock)
    ) {
      throw new Error(
        localize(
          "writeFailedUnlockUnsupported",
          "Unable to unlock file '{0}' because provider does not support it.",
          this.resourceForError(resource)
        )
      )
    }

    // Validate atomic support
    const atomic = !!options?.atomic
    if (atomic) {
      if (
        !(
          provider.capabilities & FileSystemProviderCapabilities.FileAtomicWrite
        )
      ) {
        throw new Error(
          localize(
            "writeFailedAtomicUnsupported1",
            "Unable to atomically write file '{0}' because provider does not support it.",
            this.resourceForError(resource)
          )
        )
      }

      if (
        !(provider.capabilities & FileSystemProviderCapabilities.FileReadWrite)
      ) {
        throw new Error(
          localize(
            "writeFailedAtomicUnsupported2",
            "Unable to atomically write file '{0}' because provider does not support unbuffered writes.",
            this.resourceForError(resource)
          )
        )
      }

      if (unlock) {
        throw new Error(
          localize(
            "writeFailedAtomicUnlock",
            "Unable to unlock file '{0}' because atomic write is enabled.",
            this.resourceForError(resource)
          )
        )
      }
    }

    // Validate via file stat meta data
    let stat: IStat | undefined = undefined
    try {
      stat = await provider.stat(resource)
    } catch (error) {
      return undefined // file might not exist
    }

    // File cannot be directory
    if ((stat.type & FileType.Directory) !== 0) {
      throw new FileOperationError(
        localize(
          "fileIsDirectoryWriteError",
          "Unable to write file '{0}' that is actually a directory",
          this.resourceForError(resource)
        ),
        FileOperationResult.FILE_IS_DIRECTORY,
        options
      )
    }

    // File cannot be readonly
    this.throwIfFileIsReadonly(resource, stat)

    // Dirty write prevention: if the file on disk has been changed and does not match our expected
    // mtime and etag, we bail out to prevent dirty writing.
    //
    // First, we check for a mtime that is in the future before we do more checks. The assumption is
    // that only the mtime is an indicator for a file that has changed on disk.
    //
    // Second, if the mtime has advanced, we compare the size of the file on disk with our previous
    // one using the etag() function. Relying only on the mtime check has prooven to produce false
    // positives due to file system weirdness (especially around remote file systems). As such, the
    // check for size is a weaker check because it can return a false negative if the file has changed
    // but to the same length. This is a compromise we take to avoid having to produce checksums of
    // the file content for comparison which would be much slower to compute.
    if (
      typeof options?.mtime === "number" &&
      typeof options.etag === "string" &&
      options.etag !== ETAG_DISABLED &&
      typeof stat.mtime === "number" &&
      typeof stat.size === "number" &&
      options.mtime < stat.mtime &&
      options.etag !==
        etag({
          mtime:
            options.mtime /* not using stat.mtime for a reason, see above */,
          size: stat.size,
        })
    ) {
      throw new FileOperationError(
        localize("fileModifiedError", "File Modified Since"),
        FileOperationResult.FILE_MODIFIED_SINCE,
        options
      )
    }

    return stat
  }

  async readFile(
    resource: URI,
    options?: IReadFileOptions,
    token?: CancellationToken
  ): Promise<IFileContent> {
    const provider = await this.withReadProvider(resource)

    if (options?.atomic) {
      return this.doReadFileAtomic(provider, resource, options, token)
    }

    return this.doReadFile(provider, resource, options, token)
  }

  private async doReadFileAtomic(
    provider:
      | IFileSystemProviderWithFileReadWriteCapability
      | IFileSystemProviderWithOpenReadWriteCloseCapability
      | IFileSystemProviderWithFileReadStreamCapability,
    resource: URI,
    options?: IReadFileOptions,
    token?: CancellationToken
  ): Promise<IFileContent> {
    return new Promise<IFileContent>((resolve, reject) => {
      this.writeQueue.queueFor(
        resource,
        async () => {
          try {
            const content = await this.doReadFile(
              provider,
              resource,
              options,
              token
            )
            resolve(content)
          } catch (error) {
            reject(error)
          }
        },
        this.getExtUri(provider).providerExtUri
      )
    })
  }

  private async doReadFile(
    provider:
      | IFileSystemProviderWithFileReadWriteCapability
      | IFileSystemProviderWithOpenReadWriteCloseCapability
      | IFileSystemProviderWithFileReadStreamCapability,
    resource: URI,
    options?: IReadFileOptions,
    token?: CancellationToken
  ): Promise<IFileContent> {
    const stream = await this.doReadFileStream(
      provider,
      resource,
      {
        ...options,
        // optimization: since we know that the caller does not
        // care about buffering, we indicate this to the reader.
        // this reduces all the overhead the buffered reading
        // has (open, read, close) if the provider supports
        // unbuffered reading.
        preferUnbuffered: true,
      },
      token
    )

    return {
      ...stream,
      value: await streamToBuffer(stream.value),
    }
  }

  async readFileStream(
    resource: URI,
    options?: IReadFileStreamOptions,
    token?: CancellationToken
  ): Promise<IFileStreamContent> {
    const provider = await this.withReadProvider(resource)

    return this.doReadFileStream(provider, resource, options, token)
  }

  private async doReadFileStream(
    provider:
      | IFileSystemProviderWithFileReadWriteCapability
      | IFileSystemProviderWithOpenReadWriteCloseCapability
      | IFileSystemProviderWithFileReadStreamCapability,
    resource: URI,
    options?: IReadFileOptions &
      IReadFileStreamOptions & { preferUnbuffered?: boolean },
    token?: CancellationToken
  ): Promise<IFileStreamContent> {
    // install a cancellation token that gets cancelled
    // when any error occurs. this allows us to resolve
    // the content of the file while resolving metadata
    // but still cancel the operation in certain cases.
    //
    // in addition, we pass the optional token in that
    // we got from the outside to even allow for external
    // cancellation of the read operation.
    const cancellableSource = new CancellationTokenSource(token)

    let readFileOptions = options
    if (
      hasFileAtomicReadCapability(provider) &&
      provider.enforceAtomicReadFile?.(resource)
    ) {
      readFileOptions = { ...options, atomic: true }
    }

    // validate read operation
    const statPromise = this.validateReadFile(resource, readFileOptions).then(
      (stat) => stat,
      (error) => {
        cancellableSource.dispose(true)

        throw error
      }
    )

    let fileStream: VSBufferReadableStream | undefined = undefined
    try {
      // if the etag is provided, we await the result of the validation
      // due to the likelihood of hitting a NOT_MODIFIED_SINCE result.
      // otherwise, we let it run in parallel to the file reading for
      // optimal startup performance.
      if (
        typeof readFileOptions?.etag === "string" &&
        readFileOptions.etag !== ETAG_DISABLED
      ) {
        await statPromise
      }

      // read unbuffered
      if (
        (readFileOptions?.atomic && hasFileAtomicReadCapability(provider)) || // atomic reads are always unbuffered
        !(
          hasOpenReadWriteCloseCapability(provider) ||
          hasFileReadStreamCapability(provider)
        ) || // provider has no buffered capability
        (hasReadWriteCapability(provider) && readFileOptions?.preferUnbuffered) // unbuffered read is preferred
      ) {
        fileStream = this.readFileUnbuffered(
          provider,
          resource,
          readFileOptions
        )
      }

      // read streamed (always prefer over primitive buffered read)
      else if (hasFileReadStreamCapability(provider)) {
        fileStream = this.readFileStreamed(
          provider,
          resource,
          cancellableSource.token,
          readFileOptions
        )
      }

      // read buffered
      else {
        fileStream = this.readFileBuffered(
          provider,
          resource,
          cancellableSource.token,
          readFileOptions
        )
      }

      fileStream.on("end", () => cancellableSource.dispose())
      fileStream.on("error", () => cancellableSource.dispose())

      const fileStat = await statPromise

      return {
        ...fileStat,
        value: fileStream,
      }
    } catch (error) {
      // Await the stream to finish so that we exit this method
      // in a consistent state with file handles closed
      // (https://github.com/microsoft/vscode/issues/114024)
      if (fileStream) {
        await consumeStream(fileStream)
      }

      // Re-throw errors as file operation errors but preserve
      // specific errors (such as not modified since)
      throw this.restoreReadError(error, resource, readFileOptions)
    }
  }

  private restoreReadError(
    error: Error,
    resource: URI,
    options?: IReadFileStreamOptions
  ): FileOperationError {
    const message = localize(
      "err.read",
      "Unable to read file '{0}' ({1})",
      this.resourceForError(resource),
      ensureFileSystemProviderError(error).toString()
    )

    if (error instanceof NotModifiedSinceFileOperationError) {
      return new NotModifiedSinceFileOperationError(
        message,
        error.stat,
        options
      )
    }

    if (error instanceof TooLargeFileOperationError) {
      return new TooLargeFileOperationError(
        message,
        error.fileOperationResult,
        error.size,
        error.options as IReadFileOptions
      )
    }

    return new FileOperationError(
      message,
      toFileOperationResult(error),
      options
    )
  }

  private readFileStreamed(
    provider: IFileSystemProviderWithFileReadStreamCapability,
    resource: URI,
    token: CancellationToken,
    options: IReadFileStreamOptions = Object.create(null)
  ): VSBufferReadableStream {
    const fileStream = provider.readFileStream(resource, options, token)

    return transform(
      fileStream,
      {
        data: (data) => (data instanceof VSBuffer ? data : VSBuffer.wrap(data)),
        error: (error) => this.restoreReadError(error, resource, options),
      },
      (data) => VSBuffer.concat(data)
    )
  }

  private readFileBuffered(
    provider: IFileSystemProviderWithOpenReadWriteCloseCapability,
    resource: URI,
    token: CancellationToken,
    options: IReadFileStreamOptions = Object.create(null)
  ): VSBufferReadableStream {
    const stream = newWriteableBufferStream()

    readFileIntoStream(
      provider,
      resource,
      stream,
      (data) => data,
      {
        ...options,
        bufferSize: this.BUFFER_SIZE,
        errorTransformer: (error) =>
          this.restoreReadError(error, resource, options),
      },
      token
    )

    return stream
  }

  private readFileUnbuffered(
    provider:
      | IFileSystemProviderWithFileReadWriteCapability
      | IFileSystemProviderWithFileAtomicReadCapability,
    resource: URI,
    options?: IReadFileOptions & IReadFileStreamOptions
  ): VSBufferReadableStream {
    const stream = newWriteableStream<VSBuffer>((data) => VSBuffer.concat(data))

    // Read the file into the stream async but do not wait for
    // this to complete because streams work via events
    ;(async () => {
      try {
        let buffer: Uint8Array
        if (options?.atomic && hasFileAtomicReadCapability(provider)) {
          buffer = await provider.readFile(resource, { atomic: true })
        } else {
          buffer = await provider.readFile(resource)
        }

        // respect position option
        if (typeof options?.position === "number") {
          buffer = buffer.slice(options.position)
        }

        // respect length option
        if (typeof options?.length === "number") {
          buffer = buffer.slice(0, options.length)
        }

        // Throw if file is too large to load
        this.validateReadFileLimits(resource, buffer.byteLength, options)

        // End stream with data
        stream.end(VSBuffer.wrap(buffer))
      } catch (err) {
        stream.error(err)
        stream.end()
      }
    })()

    return stream
  }

  private async validateReadFile(
    resource: URI,
    options?: IReadFileStreamOptions
  ): Promise<IFileStatWithMetadata> {
    const stat = await this.resolve(resource, { resolveMetadata: true })

    // Throw if resource is a directory
    if (stat.isDirectory) {
      throw new FileOperationError(
        localize(
          "fileIsDirectoryReadError",
          "Unable to read file '{0}' that is actually a directory",
          this.resourceForError(resource)
        ),
        FileOperationResult.FILE_IS_DIRECTORY,
        options
      )
    }

    // Throw if file not modified since (unless disabled)
    if (
      typeof options?.etag === "string" &&
      options.etag !== ETAG_DISABLED &&
      options.etag === stat.etag
    ) {
      throw new NotModifiedSinceFileOperationError(
        localize("fileNotModifiedError", "File not modified since"),
        stat,
        options
      )
    }

    // Throw if file is too large to load
    this.validateReadFileLimits(resource, stat.size, options)

    return stat
  }

  private validateReadFileLimits(
    resource: URI,
    size: number,
    options?: IReadFileStreamOptions
  ): void {
    if (
      typeof options?.limits?.size === "number" &&
      size > options.limits.size
    ) {
      throw new TooLargeFileOperationError(
        localize(
          "fileTooLargeError",
          "Unable to read file '{0}' that is too large to open",
          this.resourceForError(resource)
        ),
        FileOperationResult.FILE_TOO_LARGE,
        size,
        options
      )
    }
  }

  //#endregion

  //#region Move/Copy/Delete/Create Folder

  async canMove(
    source: URI,
    target: URI,
    overwrite?: boolean
  ): Promise<Error | true> {
    return this.doCanMoveCopy(source, target, "move", overwrite)
  }

  async canCopy(
    source: URI,
    target: URI,
    overwrite?: boolean
  ): Promise<Error | true> {
    return this.doCanMoveCopy(source, target, "copy", overwrite)
  }

  private async doCanMoveCopy(
    source: URI,
    target: URI,
    mode: "move" | "copy",
    overwrite?: boolean
  ): Promise<Error | true> {
    if (source.toString() !== target.toString()) {
      try {
        const sourceProvider =
          mode === "move"
            ? this.throwIfFileSystemIsReadonly(
                await this.withWriteProvider(source),
                source
              )
            : await this.withReadProvider(source)
        const targetProvider = this.throwIfFileSystemIsReadonly(
          await this.withWriteProvider(target),
          target
        )

        await this.doValidateMoveCopy(
          sourceProvider,
          source,
          targetProvider,
          target,
          mode,
          overwrite
        )
      } catch (error) {
        return error
      }
    }

    return true
  }

  async move(
    source: URI,
    target: URI,
    overwrite?: boolean
  ): Promise<IFileStatWithMetadata> {
    const sourceProvider = this.throwIfFileSystemIsReadonly(
      await this.withWriteProvider(source),
      source
    )
    const targetProvider = this.throwIfFileSystemIsReadonly(
      await this.withWriteProvider(target),
      target
    )

    // move
    const mode = await this.doMoveCopy(
      sourceProvider,
      source,
      targetProvider,
      target,
      "move",
      !!overwrite
    )

    // resolve and send events
    const fileStat = await this.resolve(target, { resolveMetadata: true })
    this._onDidRunOperation.fire(
      new FileOperationEvent(
        source,
        mode === "move" ? FileOperation.MOVE : FileOperation.COPY,
        fileStat
      )
    )

    return fileStat
  }

  async copy(
    source: URI,
    target: URI,
    overwrite?: boolean
  ): Promise<IFileStatWithMetadata> {
    const sourceProvider = await this.withReadProvider(source)
    const targetProvider = this.throwIfFileSystemIsReadonly(
      await this.withWriteProvider(target),
      target
    )

    // copy
    const mode = await this.doMoveCopy(
      sourceProvider,
      source,
      targetProvider,
      target,
      "copy",
      !!overwrite
    )

    // resolve and send events
    const fileStat = await this.resolve(target, { resolveMetadata: true })
    this._onDidRunOperation.fire(
      new FileOperationEvent(
        source,
        mode === "copy" ? FileOperation.COPY : FileOperation.MOVE,
        fileStat
      )
    )

    return fileStat
  }

  private async doMoveCopy(
    sourceProvider: IFileSystemProvider,
    source: URI,
    targetProvider: IFileSystemProvider,
    target: URI,
    mode: "move" | "copy",
    overwrite: boolean
  ): Promise<"move" | "copy"> {
    if (source.toString() === target.toString()) {
      return mode // simulate node.js behaviour here and do a no-op if paths match
    }

    // validation
    const { exists, isSameResourceWithDifferentPathCase } =
      await this.doValidateMoveCopy(
        sourceProvider,
        source,
        targetProvider,
        target,
        mode,
        overwrite
      )

    // delete as needed (unless target is same resurce with different path case)
    if (exists && !isSameResourceWithDifferentPathCase && overwrite) {
      await this.del(target, { recursive: true })
    }

    // create parent folders
    await this.mkdirp(
      targetProvider,
      this.getExtUri(targetProvider).providerExtUri.dirname(target)
    )

    // copy source => target
    if (mode === "copy") {
      // same provider with fast copy: leverage copy() functionality
      if (
        sourceProvider === targetProvider &&
        hasFileFolderCopyCapability(sourceProvider)
      ) {
        await sourceProvider.copy(source, target, { overwrite })
      }

      // when copying via buffer/unbuffered, we have to manually
      // traverse the source if it is a folder and not a file
      else {
        const sourceFile = await this.resolve(source)
        if (sourceFile.isDirectory) {
          await this.doCopyFolder(
            sourceProvider,
            sourceFile,
            targetProvider,
            target
          )
        } else {
          await this.doCopyFile(sourceProvider, source, targetProvider, target)
        }
      }

      return mode
    }

    // move source => target
    else {
      // same provider: leverage rename() functionality
      if (sourceProvider === targetProvider) {
        await sourceProvider.rename(source, target, { overwrite })

        return mode
      }

      // across providers: copy to target & delete at source
      else {
        await this.doMoveCopy(
          sourceProvider,
          source,
          targetProvider,
          target,
          "copy",
          overwrite
        )
        await this.del(source, { recursive: true })

        return "copy"
      }
    }
  }

  private async doCopyFile(
    sourceProvider: IFileSystemProvider,
    source: URI,
    targetProvider: IFileSystemProvider,
    target: URI
  ): Promise<void> {
    // copy: source (buffered) => target (buffered)
    if (
      hasOpenReadWriteCloseCapability(sourceProvider) &&
      hasOpenReadWriteCloseCapability(targetProvider)
    ) {
      return this.doPipeBuffered(sourceProvider, source, targetProvider, target)
    }

    // copy: source (buffered) => target (unbuffered)
    if (
      hasOpenReadWriteCloseCapability(sourceProvider) &&
      hasReadWriteCapability(targetProvider)
    ) {
      return this.doPipeBufferedToUnbuffered(
        sourceProvider,
        source,
        targetProvider,
        target
      )
    }

    // copy: source (unbuffered) => target (buffered)
    if (
      hasReadWriteCapability(sourceProvider) &&
      hasOpenReadWriteCloseCapability(targetProvider)
    ) {
      return this.doPipeUnbufferedToBuffered(
        sourceProvider,
        source,
        targetProvider,
        target
      )
    }

    // copy: source (unbuffered) => target (unbuffered)
    if (
      hasReadWriteCapability(sourceProvider) &&
      hasReadWriteCapability(targetProvider)
    ) {
      return this.doPipeUnbuffered(
        sourceProvider,
        source,
        targetProvider,
        target
      )
    }
  }

  private async doCopyFolder(
    sourceProvider: IFileSystemProvider,
    sourceFolder: IFileStat,
    targetProvider: IFileSystemProvider,
    targetFolder: URI
  ): Promise<void> {
    // create folder in target
    await targetProvider.mkdir(targetFolder)

    // create children in target
    if (Array.isArray(sourceFolder.children)) {
      await Promises.settled(
        sourceFolder.children.map(async (sourceChild) => {
          const targetChild = this.getExtUri(
            targetProvider
          ).providerExtUri.joinPath(targetFolder, sourceChild.name)
          if (sourceChild.isDirectory) {
            return this.doCopyFolder(
              sourceProvider,
              await this.resolve(sourceChild.resource),
              targetProvider,
              targetChild
            )
          } else {
            return this.doCopyFile(
              sourceProvider,
              sourceChild.resource,
              targetProvider,
              targetChild
            )
          }
        })
      )
    }
  }

  private async doValidateMoveCopy(
    sourceProvider: IFileSystemProvider,
    source: URI,
    targetProvider: IFileSystemProvider,
    target: URI,
    mode: "move" | "copy",
    overwrite?: boolean
  ): Promise<{
    exists: boolean
    isSameResourceWithDifferentPathCase: boolean
  }> {
    let isSameResourceWithDifferentPathCase = false

    // Check if source is equal or parent to target (requires providers to be the same)
    if (sourceProvider === targetProvider) {
      const { providerExtUri, isPathCaseSensitive } =
        this.getExtUri(sourceProvider)
      if (!isPathCaseSensitive) {
        isSameResourceWithDifferentPathCase = providerExtUri.isEqual(
          source,
          target
        )
      }

      if (isSameResourceWithDifferentPathCase && mode === "copy") {
        throw new Error(
          localize(
            "unableToMoveCopyError1",
            "Unable to copy when source '{0}' is same as target '{1}' with different path case on a case insensitive file system",
            this.resourceForError(source),
            this.resourceForError(target)
          )
        )
      }

      if (
        !isSameResourceWithDifferentPathCase &&
        providerExtUri.isEqualOrParent(target, source)
      ) {
        throw new Error(
          localize(
            "unableToMoveCopyError2",
            "Unable to move/copy when source '{0}' is parent of target '{1}'.",
            this.resourceForError(source),
            this.resourceForError(target)
          )
        )
      }
    }

    // Extra checks if target exists and this is not a rename
    const exists = await this.exists(target)
    if (exists && !isSameResourceWithDifferentPathCase) {
      // Bail out if target exists and we are not about to overwrite
      if (!overwrite) {
        throw new FileOperationError(
          localize(
            "unableToMoveCopyError3",
            "Unable to move/copy '{0}' because target '{1}' already exists at destination.",
            this.resourceForError(source),
            this.resourceForError(target)
          ),
          FileOperationResult.FILE_MOVE_CONFLICT
        )
      }

      // Special case: if the target is a parent of the source, we cannot delete
      // it as it would delete the source as well. In this case we have to throw
      if (sourceProvider === targetProvider) {
        const { providerExtUri } = this.getExtUri(sourceProvider)
        if (providerExtUri.isEqualOrParent(source, target)) {
          throw new Error(
            localize(
              "unableToMoveCopyError4",
              "Unable to move/copy '{0}' into '{1}' since a file would replace the folder it is contained in.",
              this.resourceForError(source),
              this.resourceForError(target)
            )
          )
        }
      }
    }

    return { exists, isSameResourceWithDifferentPathCase }
  }

  private getExtUri(provider: IFileSystemProvider): {
    providerExtUri: IExtUri
    isPathCaseSensitive: boolean
  } {
    const isPathCaseSensitive = this.isPathCaseSensitive(provider)

    return {
      providerExtUri: isPathCaseSensitive ? extUri : extUriIgnorePathCase,
      isPathCaseSensitive,
    }
  }

  private isPathCaseSensitive(provider: IFileSystemProvider): boolean {
    return !!(
      provider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive
    )
  }

  async createFolder(resource: URI): Promise<IFileStatWithMetadata> {
    const provider = this.throwIfFileSystemIsReadonly(
      await this.withProvider(resource),
      resource
    )

    // mkdir recursively
    await this.mkdirp(provider, resource)

    // events
    const fileStat = await this.resolve(resource, { resolveMetadata: true })
    this._onDidRunOperation.fire(
      new FileOperationEvent(resource, FileOperation.CREATE, fileStat)
    )

    return fileStat
  }

  private async mkdirp(
    provider: IFileSystemProvider,
    directory: URI
  ): Promise<void> {
    const directoriesToCreate: string[] = []

    // mkdir until we reach root
    const { providerExtUri } = this.getExtUri(provider)
    while (
      !providerExtUri.isEqual(directory, providerExtUri.dirname(directory))
    ) {
      try {
        const stat = await provider.stat(directory)
        if ((stat.type & FileType.Directory) === 0) {
          throw new Error(
            localize(
              "mkdirExistsError",
              "Unable to create folder '{0}' that already exists but is not a directory",
              this.resourceForError(directory)
            )
          )
        }

        break // we have hit a directory that exists -> good
      } catch (error) {
        // Bubble up any other error that is not file not found
        if (
          toFileSystemProviderErrorCode(error) !==
          FileSystemProviderErrorCode.FileNotFound
        ) {
          throw error
        }

        // Upon error, remember directories that need to be created
        directoriesToCreate.push(providerExtUri.basename(directory))

        // Continue up
        directory = providerExtUri.dirname(directory)
      }
    }

    // Create directories as needed
    for (let i = directoriesToCreate.length - 1; i >= 0; i--) {
      directory = providerExtUri.joinPath(directory, directoriesToCreate[i])

      try {
        await provider.mkdir(directory)
      } catch (error) {
        if (
          toFileSystemProviderErrorCode(error) !==
          FileSystemProviderErrorCode.FileExists
        ) {
          // For mkdirp() we tolerate that the mkdir() call fails
          // in case the folder already exists. This follows node.js
          // own implementation of fs.mkdir({ recursive: true }) and
          // reduces the chances of race conditions leading to errors
          // if multiple calls try to create the same folders
          // As such, we only throw an error here if it is other than
          // the fact that the file already exists.
          // (see also https://github.com/microsoft/vscode/issues/89834)
          throw error
        }
      }
    }
  }

  async canDelete(
    resource: URI,
    options?: Partial<IFileDeleteOptions>
  ): Promise<Error | true> {
    try {
      await this.doValidateDelete(resource, options)
    } catch (error) {
      return error
    }

    return true
  }

  private async doValidateDelete(
    resource: URI,
    options?: Partial<IFileDeleteOptions>
  ): Promise<IFileSystemProvider> {
    const provider = this.throwIfFileSystemIsReadonly(
      await this.withProvider(resource),
      resource
    )

    // Validate trash support
    const useTrash = !!options?.useTrash
    if (
      useTrash &&
      !(provider.capabilities & FileSystemProviderCapabilities.Trash)
    ) {
      throw new Error(
        localize(
          "deleteFailedTrashUnsupported",
          "Unable to delete file '{0}' via trash because provider does not support it.",
          this.resourceForError(resource)
        )
      )
    }

    // Validate atomic support
    const atomic = options?.atomic
    if (
      atomic &&
      !(provider.capabilities & FileSystemProviderCapabilities.FileAtomicDelete)
    ) {
      throw new Error(
        localize(
          "deleteFailedAtomicUnsupported",
          "Unable to delete file '{0}' atomically because provider does not support it.",
          this.resourceForError(resource)
        )
      )
    }

    if (useTrash && atomic) {
      throw new Error(
        localize(
          "deleteFailedTrashAndAtomicUnsupported",
          "Unable to atomically delete file '{0}' because using trash is enabled.",
          this.resourceForError(resource)
        )
      )
    }

    // Validate delete
    let stat: IStat | undefined = undefined
    try {
      stat = await provider.stat(resource)
    } catch (error) {
      // Handled later
    }

    if (stat) {
      this.throwIfFileIsReadonly(resource, stat)
    } else {
      throw new FileOperationError(
        localize(
          "deleteFailedNotFound",
          "Unable to delete nonexistent file '{0}'",
          this.resourceForError(resource)
        ),
        FileOperationResult.FILE_NOT_FOUND
      )
    }

    // Validate recursive
    const recursive = !!options?.recursive
    if (!recursive) {
      const stat = await this.resolve(resource)
      if (
        stat.isDirectory &&
        Array.isArray(stat.children) &&
        stat.children.length > 0
      ) {
        throw new Error(
          localize(
            "deleteFailedNonEmptyFolder",
            "Unable to delete non-empty folder '{0}'.",
            this.resourceForError(resource)
          )
        )
      }
    }

    return provider
  }

  async del(
    resource: URI,
    options?: Partial<IFileDeleteOptions>
  ): Promise<void> {
    const provider = await this.doValidateDelete(resource, options)

    let deleteFileOptions = options
    if (hasFileAtomicDeleteCapability(provider) && !deleteFileOptions?.atomic) {
      const enforcedAtomicDelete = provider.enforceAtomicDelete?.(resource)
      if (enforcedAtomicDelete) {
        deleteFileOptions = { ...options, atomic: enforcedAtomicDelete }
      }
    }

    const useTrash = !!deleteFileOptions?.useTrash
    const recursive = !!deleteFileOptions?.recursive
    const atomic = deleteFileOptions?.atomic ?? false

    // Delete through provider
    await provider.delete(resource, { recursive, useTrash, atomic })

    // Events
    this._onDidRunOperation.fire(
      new FileOperationEvent(resource, FileOperation.DELETE)
    )
  }

  //#endregion

  //#region Clone File

  async cloneFile(source: URI, target: URI): Promise<void> {
    const sourceProvider = await this.withProvider(source)
    const targetProvider = this.throwIfFileSystemIsReadonly(
      await this.withWriteProvider(target),
      target
    )

    if (
      sourceProvider === targetProvider &&
      this.getExtUri(sourceProvider).providerExtUri.isEqual(source, target)
    ) {
      return // return early if paths are equal
    }

    // same provider, use `cloneFile` when native support is provided
    if (
      sourceProvider === targetProvider &&
      hasFileCloneCapability(sourceProvider)
    ) {
      return sourceProvider.cloneFile(source, target)
    }

    // otherwise, either providers are different or there is no native
    // `cloneFile` support, then we fallback to emulate a clone as best
    // as we can with the other primitives

    // create parent folders
    await this.mkdirp(
      targetProvider,
      this.getExtUri(targetProvider).providerExtUri.dirname(target)
    )

    // leverage `copy` method if provided and providers are identical
    // queue on the source to ensure atomic read
    if (
      sourceProvider === targetProvider &&
      hasFileFolderCopyCapability(sourceProvider)
    ) {
      return this.writeQueue.queueFor(
        source,
        () => sourceProvider.copy(source, target, { overwrite: true }),
        this.getExtUri(sourceProvider).providerExtUri
      )
    }

    // otherwise copy via buffer/unbuffered and use a write queue
    // on the source to ensure atomic operation as much as possible
    return this.writeQueue.queueFor(
      source,
      () => this.doCopyFile(sourceProvider, source, targetProvider, target),
      this.getExtUri(sourceProvider).providerExtUri
    )
  }

  //#endregion

  //#region File Watching

  private readonly internalOnDidFilesChange = this._register(
    new Emitter<FileChangesEvent>()
  )

  private readonly _onDidUncorrelatedFilesChange = this._register(
    new Emitter<FileChangesEvent>()
  )
  readonly onDidFilesChange = this._onDidUncorrelatedFilesChange.event // global `onDidFilesChange` skips correlated events

  private readonly _onDidWatchError = this._register(new Emitter<Error>())
  readonly onDidWatchError = this._onDidWatchError.event

  private readonly activeWatchers = new Map<
    number /* watch request hash */,
    { disposable: IDisposable; count: number }
  >()

  private static WATCHER_CORRELATION_IDS = 0

  createWatcher(
    resource: URI,
    options: IWatchOptionsWithoutCorrelation
  ): IFileSystemWatcher {
    return this.watch(resource, {
      ...options,
      // Explicitly set a correlation id so that file events that originate
      // from requests from extensions are exclusively routed back to the
      // extension host and not into the workbench.
      correlationId: FileService.WATCHER_CORRELATION_IDS++,
    })
  }

  watch(
    resource: URI,
    options: IWatchOptionsWithCorrelation
  ): IFileSystemWatcher
  watch(resource: URI, options?: IWatchOptionsWithoutCorrelation): IDisposable
  watch(
    resource: URI,
    options: IWatchOptions = { recursive: false, excludes: [] }
  ): IFileSystemWatcher | IDisposable {
    const disposables = new DisposableStore()

    // Forward watch request to provider and wire in disposables
    let watchDisposed = false
    let disposeWatch = () => {
      watchDisposed = true
    }
    disposables.add(toDisposable(() => disposeWatch()))

    // Watch and wire in disposable which is async but
    // check if we got disposed meanwhile and forward
    ;(async () => {
      try {
        const disposable = await this.doWatch(resource, options)
        if (watchDisposed) {
          dispose(disposable)
        } else {
          disposeWatch = () => dispose(disposable)
        }
      } catch (error) {
        this.logService.error(error)
      }
    })()

    // When a correlation identifier is set, return a specific
    // watcher that only emits events matching that correalation.
    const correlationId = options.correlationId
    if (typeof correlationId === "number") {
      const fileChangeEmitter = disposables.add(new Emitter<FileChangesEvent>())
      disposables.add(
        this.internalOnDidFilesChange.event((e) => {
          if (e.correlates(correlationId)) {
            fileChangeEmitter.fire(e)
          }
        })
      )

      const watcher: IFileSystemWatcher = {
        onDidChange: fileChangeEmitter.event,
        dispose: () => disposables.dispose(),
      }

      return watcher
    }

    return disposables
  }

  private async doWatch(
    resource: URI,
    options: IWatchOptions
  ): Promise<IDisposable> {
    const provider = await this.withProvider(resource)

    // Deduplicate identical watch requests
    const watchHash = hash([
      this.getExtUri(provider).providerExtUri.getComparisonKey(resource),
      options,
    ])
    let watcher = this.activeWatchers.get(watchHash)
    if (!watcher) {
      watcher = {
        count: 0,
        disposable: provider.watch(resource, options),
      }

      this.activeWatchers.set(watchHash, watcher)
    }

    // Increment usage counter
    watcher.count += 1

    return toDisposable(() => {
      if (watcher) {
        // Unref
        watcher.count--

        // Dispose only when last user is reached
        if (watcher.count === 0) {
          dispose(watcher.disposable)
          this.activeWatchers.delete(watchHash)
        }
      }
    })
  }

  override dispose(): void {
    super.dispose()

    for (const [, watcher] of this.activeWatchers) {
      dispose(watcher.disposable)
    }

    this.activeWatchers.clear()
  }

  //#endregion

  //#region Helpers

  private readonly writeQueue = this._register(new ResourceQueue())

  private async doWriteBuffered(
    provider: IFileSystemProviderWithOpenReadWriteCloseCapability,
    resource: URI,
    options: IWriteFileOptions | undefined,
    readableOrStreamOrBufferedStream:
      | VSBufferReadable
      | VSBufferReadableStream
      | VSBufferReadableBufferedStream
  ): Promise<void> {
    return this.writeQueue.queueFor(
      resource,
      async () => {
        // open handle
        const handle = await provider.open(resource, {
          create: true,
          unlock: options?.unlock ?? false,
        })

        // write into handle until all bytes from buffer have been written
        try {
          if (
            isReadableStream(readableOrStreamOrBufferedStream) ||
            isReadableBufferedStream(readableOrStreamOrBufferedStream)
          ) {
            await this.doWriteStreamBufferedQueued(
              provider,
              handle,
              readableOrStreamOrBufferedStream
            )
          } else {
            await this.doWriteReadableBufferedQueued(
              provider,
              handle,
              readableOrStreamOrBufferedStream
            )
          }
        } catch (error) {
          throw ensureFileSystemProviderError(error)
        } finally {
          // close handle always
          await provider.close(handle)
        }
      },
      this.getExtUri(provider).providerExtUri
    )
  }

  private async doWriteStreamBufferedQueued(
    provider: IFileSystemProviderWithOpenReadWriteCloseCapability,
    handle: number,
    streamOrBufferedStream:
      | VSBufferReadableStream
      | VSBufferReadableBufferedStream
  ): Promise<void> {
    let posInFile = 0
    let stream: VSBufferReadableStream

    // Buffered stream: consume the buffer first by writing
    // it to the target before reading from the stream.
    if (isReadableBufferedStream(streamOrBufferedStream)) {
      if (streamOrBufferedStream.buffer.length > 0) {
        const chunk = VSBuffer.concat(streamOrBufferedStream.buffer)
        await this.doWriteBuffer(
          provider,
          handle,
          chunk,
          chunk.byteLength,
          posInFile,
          0
        )

        posInFile += chunk.byteLength
      }

      // If the stream has been consumed, return early
      if (streamOrBufferedStream.ended) {
        return
      }

      stream = streamOrBufferedStream.stream
    }

    // Unbuffered stream - just take as is
    else {
      stream = streamOrBufferedStream
    }

    return new Promise((resolve, reject) => {
      listenStream(stream, {
        onData: async (chunk) => {
          // pause stream to perform async write operation
          stream.pause()

          try {
            await this.doWriteBuffer(
              provider,
              handle,
              chunk,
              chunk.byteLength,
              posInFile,
              0
            )
          } catch (error) {
            return reject(error)
          }

          posInFile += chunk.byteLength

          // resume stream now that we have successfully written
          // run this on the next tick to prevent increasing the
          // execution stack because resume() may call the event
          // handler again before finishing.
          setTimeout(() => stream.resume())
        },
        onError: (error) => reject(error),
        onEnd: () => resolve(),
      })
    })
  }

  private async doWriteReadableBufferedQueued(
    provider: IFileSystemProviderWithOpenReadWriteCloseCapability,
    handle: number,
    readable: VSBufferReadable
  ): Promise<void> {
    let posInFile = 0

    let chunk: VSBuffer | null
    while ((chunk = readable.read()) !== null) {
      await this.doWriteBuffer(
        provider,
        handle,
        chunk,
        chunk.byteLength,
        posInFile,
        0
      )

      posInFile += chunk.byteLength
    }
  }

  private async doWriteBuffer(
    provider: IFileSystemProviderWithOpenReadWriteCloseCapability,
    handle: number,
    buffer: VSBuffer,
    length: number,
    posInFile: number,
    posInBuffer: number
  ): Promise<void> {
    let totalBytesWritten = 0
    while (totalBytesWritten < length) {
      // Write through the provider
      const bytesWritten = await provider.write(
        handle,
        posInFile + totalBytesWritten,
        buffer.buffer,
        posInBuffer + totalBytesWritten,
        length - totalBytesWritten
      )
      totalBytesWritten += bytesWritten
    }
  }

  private async doWriteUnbuffered(
    provider: IFileSystemProviderWithFileReadWriteCapability,
    resource: URI,
    options: IWriteFileOptions | undefined,
    bufferOrReadableOrStreamOrBufferedStream:
      | VSBuffer
      | VSBufferReadable
      | VSBufferReadableStream
      | VSBufferReadableBufferedStream
  ): Promise<void> {
    return this.writeQueue.queueFor(
      resource,
      () =>
        this.doWriteUnbufferedQueued(
          provider,
          resource,
          options,
          bufferOrReadableOrStreamOrBufferedStream
        ),
      this.getExtUri(provider).providerExtUri
    )
  }

  private async doWriteUnbufferedQueued(
    provider: IFileSystemProviderWithFileReadWriteCapability,
    resource: URI,
    options: IWriteFileOptions | undefined,
    bufferOrReadableOrStreamOrBufferedStream:
      | VSBuffer
      | VSBufferReadable
      | VSBufferReadableStream
      | VSBufferReadableBufferedStream
  ): Promise<void> {
    let buffer: VSBuffer
    if (bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer) {
      buffer = bufferOrReadableOrStreamOrBufferedStream
    } else if (isReadableStream(bufferOrReadableOrStreamOrBufferedStream)) {
      buffer = await streamToBuffer(bufferOrReadableOrStreamOrBufferedStream)
    } else if (
      isReadableBufferedStream(bufferOrReadableOrStreamOrBufferedStream)
    ) {
      buffer = await bufferedStreamToBuffer(
        bufferOrReadableOrStreamOrBufferedStream
      )
    } else {
      buffer = readableToBuffer(bufferOrReadableOrStreamOrBufferedStream)
    }

    // Write through the provider
    await provider.writeFile(resource, buffer.buffer, {
      create: true,
      overwrite: true,
      unlock: options?.unlock ?? false,
      atomic: options?.atomic ?? false,
    })
  }

  private async doPipeBuffered(
    sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability,
    source: URI,
    targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability,
    target: URI
  ): Promise<void> {
    return this.writeQueue.queueFor(
      target,
      () =>
        this.doPipeBufferedQueued(
          sourceProvider,
          source,
          targetProvider,
          target
        ),
      this.getExtUri(targetProvider).providerExtUri
    )
  }

  private async doPipeBufferedQueued(
    sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability,
    source: URI,
    targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability,
    target: URI
  ): Promise<void> {
    let sourceHandle: number | undefined = undefined
    let targetHandle: number | undefined = undefined

    try {
      // Open handles
      sourceHandle = await sourceProvider.open(source, { create: false })
      targetHandle = await targetProvider.open(target, {
        create: true,
        unlock: false,
      })

      const buffer = VSBuffer.alloc(this.BUFFER_SIZE)

      let posInFile = 0
      let posInBuffer = 0
      let bytesRead = 0
      do {
        // read from source (sourceHandle) at current position (posInFile) into buffer (buffer) at
        // buffer position (posInBuffer) up to the size of the buffer (buffer.byteLength).
        bytesRead = await sourceProvider.read(
          sourceHandle,
          posInFile,
          buffer.buffer,
          posInBuffer,
          buffer.byteLength - posInBuffer
        )

        // write into target (targetHandle) at current position (posInFile) from buffer (buffer) at
        // buffer position (posInBuffer) all bytes we read (bytesRead).
        await this.doWriteBuffer(
          targetProvider,
          targetHandle,
          buffer,
          bytesRead,
          posInFile,
          posInBuffer
        )

        posInFile += bytesRead
        posInBuffer += bytesRead

        // when buffer full, fill it again from the beginning
        if (posInBuffer === buffer.byteLength) {
          posInBuffer = 0
        }
      } while (bytesRead > 0)
    } catch (error) {
      throw ensureFileSystemProviderError(error)
    } finally {
      await Promises.settled([
        typeof sourceHandle === "number"
          ? sourceProvider.close(sourceHandle)
          : Promise.resolve(),
        typeof targetHandle === "number"
          ? targetProvider.close(targetHandle)
          : Promise.resolve(),
      ])
    }
  }

  private async doPipeUnbuffered(
    sourceProvider: IFileSystemProviderWithFileReadWriteCapability,
    source: URI,
    targetProvider: IFileSystemProviderWithFileReadWriteCapability,
    target: URI
  ): Promise<void> {
    return this.writeQueue.queueFor(
      target,
      () =>
        this.doPipeUnbufferedQueued(
          sourceProvider,
          source,
          targetProvider,
          target
        ),
      this.getExtUri(targetProvider).providerExtUri
    )
  }

  private async doPipeUnbufferedQueued(
    sourceProvider: IFileSystemProviderWithFileReadWriteCapability,
    source: URI,
    targetProvider: IFileSystemProviderWithFileReadWriteCapability,
    target: URI
  ): Promise<void> {
    return targetProvider.writeFile(
      target,
      await sourceProvider.readFile(source),
      { create: true, overwrite: true, unlock: false, atomic: false }
    )
  }

  private async doPipeUnbufferedToBuffered(
    sourceProvider: IFileSystemProviderWithFileReadWriteCapability,
    source: URI,
    targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability,
    target: URI
  ): Promise<void> {
    return this.writeQueue.queueFor(
      target,
      () =>
        this.doPipeUnbufferedToBufferedQueued(
          sourceProvider,
          source,
          targetProvider,
          target
        ),
      this.getExtUri(targetProvider).providerExtUri
    )
  }

  private async doPipeUnbufferedToBufferedQueued(
    sourceProvider: IFileSystemProviderWithFileReadWriteCapability,
    source: URI,
    targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability,
    target: URI
  ): Promise<void> {
    // Open handle
    const targetHandle = await targetProvider.open(target, {
      create: true,
      unlock: false,
    })

    // Read entire buffer from source and write buffered
    try {
      const buffer = await sourceProvider.readFile(source)
      await this.doWriteBuffer(
        targetProvider,
        targetHandle,
        VSBuffer.wrap(buffer),
        buffer.byteLength,
        0,
        0
      )
    } catch (error) {
      throw ensureFileSystemProviderError(error)
    } finally {
      await targetProvider.close(targetHandle)
    }
  }

  private async doPipeBufferedToUnbuffered(
    sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability,
    source: URI,
    targetProvider: IFileSystemProviderWithFileReadWriteCapability,
    target: URI
  ): Promise<void> {
    // Read buffer via stream buffered
    const buffer = await streamToBuffer(
      this.readFileBuffered(sourceProvider, source, CancellationToken.None)
    )

    // Write buffer into target at once
    await this.doWriteUnbuffered(targetProvider, target, undefined, buffer)
  }

  protected throwIfFileSystemIsReadonly<T extends IFileSystemProvider>(
    provider: T,
    resource: URI
  ): T {
    if (provider.capabilities & FileSystemProviderCapabilities.Readonly) {
      throw new FileOperationError(
        localize(
          "err.readonly",
          "Unable to modify read-only file '{0}'",
          this.resourceForError(resource)
        ),
        FileOperationResult.FILE_PERMISSION_DENIED
      )
    }

    return provider
  }

  private throwIfFileIsReadonly(resource: URI, stat: IStat): void {
    if ((stat.permissions ?? 0) & FilePermission.Readonly) {
      throw new FileOperationError(
        localize(
          "err.readonly",
          "Unable to modify read-only file '{0}'",
          this.resourceForError(resource)
        ),
        FileOperationResult.FILE_PERMISSION_DENIED
      )
    }
  }

  private resourceForError(resource: URI): string {
    if (resource.scheme === Schemas.file) {
      return resource.fsPath
    }

    return resource.toString(true)
  }

  //#endregion
}
